/*
 * @(#)StoreTxt.java 1.0 30 May 2007
 *
 * Copyright (c) 2005-2007 Soluciones Info Móviles S.A.C.
 * Av. Javier Prado Oeste 203 - Piso 13, Lima 27, Perú.
 * Todos los derechos reservados.
 */

package sim.store;

import sim.framework.io.*;
import sim.framework.*;
import sim.utils.*;

/**
 * Esta clase permite la creación y consulta datos tabulados en un
 * objeto de tipo File, que será llamado tabla. Esta tabla está
 * compuesta de líneas y estas están compuestas por campos.
 * <p>
 * Una línea es una cadena de caracteres terminado en #STOTXT_REG_TOKEN
 * y un campo en un conjunto de carateres delimitado por #STOTXT_FLD_TOKEN.
 * Las variables de este tipo solo puoden ser utilizadas por las
 * funciones de este modulo.
 *
 * @author      Omar Quintana
 * @since       1.0
 */
public final class StoreTxt {
    
    public static final byte[] FIELD_TOKEN = {'|'};
    
    protected Simlet m_Simlet;
    // Constantes
    private static final int MINSTEP = 64;
    private static final int HEAD_DELETE_LEN = 8;
    private static final int HEAD_NEXTID_LEN = 8;
    private static final int HEAD_LEN = HEAD_DELETE_LEN + HEAD_NEXTID_LEN + 1;
    // identificadores de elementos de la cabecera
    private static final int HEAD_FLD_DELETE = 0;
    private static final int HEAD_FLD_NEXTID = 1;
    // Variables de la cabecera
    private int     m_HeadDelete = Integer.MAX_VALUE;
    private int     m_HeadNextID = 1;
    private boolean m_SaveNextID;
    // Almacenamiento
    private FileStream  m_File;
    private byte[]      m_Data;
    // Buffers
    private Buffer  m_BufRecord;  // para escribir registros en disco
    private Buffer  m_BufShared;  // para realizar diferentes tareas
    // Scanner
    private int         m_ScanOffset;
    private StoreTxtId  m_ScanRecordID;
    private Buffer      m_ScanBuffer;  // para scanear una tabla
    
    /**
     * Constructor. Instancia un objeto <code>StoreTxt</code>, inicializa
     * <code>m_Simlet</code>.
     *
     * @param  s  simlet, la aplicación.
     */
    public StoreTxt(Simlet s) {
        m_Simlet = s;
    }
    
    /**
     * Método estático. Crea un objeto <code>StoreTxt</code>.
     *
     * @param  s        simlet, la aplicación.
     * @param  filename nombre del archivo con el cual trabajar.
     * @param  step     longitud de los saltos que se deben ir dando.
     * @param  nextid   indicador si se desea grabar en la cabecera un dato
     *                  con el siguiente ID de un registro.
     * @return          un objeto <code>StoreTxt</code>. Si no se pudiera
     *                  completar la operación retornar <code>null</code>.
     * @see             #open(String, int)
     */
    public static StoreTxt create(Simlet s, String filename, int step, boolean nextid) {
        StoreTxt newStoTxt;
        newStoTxt = new StoreTxt(s);
        if (newStoTxt.open(filename, step, nextid)) {
            return newStoTxt;
        }
        newStoTxt.close();
        return null;
    }
    
    /**
     * Método estático. Crea un objeto <code>StoreTxt</code>.
     *
     * @param  s        simlet, la aplicación.
     * @param  filename nombre del archivo con el cual trabajar.
     * @param  nextid   indicador si se desea grabar en la cabecera un dato
     *                  con el siguiente ID de un registro.
     * @return          un objeto <code>StoreTxt</code>. Si no se pudiera
     *                  completar la operación retornar <code>null</code>.
     * @see             #open(String, int)
     */
    public static StoreTxt create(Simlet s, String filename, boolean nextid) {
        return create(s, filename, MINSTEP, nextid);
    }
    
    /**
     * Método estático. Crea un objeto <code>StoreTxt</code>.
     *
     * @param  s        simlet, la aplicación.
     * @param  filename nombre del archivo con el cual trabajar.
     * @return          un objeto <code>StoreTxt</code>. Si no se pudiera
     *                  completar la operación retornar <code>null</code>.
     * @see             #open(String, int)
     */
    public static StoreTxt create(Simlet s, String filename) {
        return create(s, filename, MINSTEP, false);
    }
    
    /**
     * Destructor. Libera la memoria asignada al objeto instanciado
     * en la clase.
     */
    public void close() {
        scanStop();
        m_File.flush();
        m_File.close();
        m_File = null;
        m_BufRecord = null;
        m_BufShared = null;
        m_ScanBuffer = null;
        m_Data = null;
        m_ScanRecordID = null;
    }
    
    /**
     * Abre una tabla mediante el uso de <code>File</code>, asimismo
     * realiza los trabajos necesarios con la cabecera de la tabla.
     *
     * @param  s        simlet, la aplicación.
     * @param  filename nombre del archivo con el cual trabajar.
     * @param  step     longitud de los saltos que se deben ir dando.
     * @param  nextid   indicador si se desea grabar en la cabecera un dato
     *                  con el siguiente ID de un registro.
     * @return          <code>true</code> si se pudo abrir el objeto
     *                  satisfactoriamente.
     *                  <code>false</code> en caso contrario.
     * @see             sim.framework.io.FileStream#create(Simlet, String, byte)
     * @see             #headLoad()
     * @see             #scanStop()
     */
    public boolean open(final String filename, final int step, boolean nextid) {
        m_Data   = new byte[(step < MINSTEP ? MINSTEP : step)];
        m_File = (FileStream) FileStream.create(m_Simlet,
                filename, File.MODE_WRITEEX);
        if(m_File != null) {
            m_SaveNextID = nextid;
            if (headLoad()) {
                scanStop();
                return true;
            }
        }
        return false;
    }
    
    /**
     * Carga los datos de la cabecera de la tabla.
     *
     * @see   #headInit()
     * @see   #headRead()
     */
    private boolean headLoad() {
        int lenght;
        lenght = m_File.gets(m_Data);
        if (lenght >= HEAD_LEN + Utils.ENDOFLINE.length) {
            // offset de elemento "borrado"
            m_HeadDelete = Utils.memtoul(m_Data, 0, HEAD_DELETE_LEN, 16);
            // siguiente ID
            if (m_SaveNextID) {
                m_HeadNextID = Utils.memtoul(m_Data, HEAD_DELETE_LEN + 1, HEAD_NEXTID_LEN, 16);
            }
            return true;
        }
        return headSave();
    }
    
    /**
     * Guarda en disco la cabecera de la tabla.
     *
     * @see   sim.framework.io.FileStream#write()
     */
    private boolean headSave() {
        // offset de elemento "borrado"
        Utils.fmtInteger(m_Data, HEAD_DELETE_LEN, m_HeadDelete, 16, 0, Utils.FMT_OHEX);
        if (m_SaveNextID) {
            // separador de elementos
            Utils.memcpy(m_Data, HEAD_DELETE_LEN, Utils.TOKEN, Utils.TOKEN_PIPE, 1);
            // siguiente ID
            Utils.fmtInteger(m_Data, HEAD_NEXTID_LEN + 1, 4, m_HeadNextID, 16, 0, Utils.FMT_LEFT);
        }
        // separador de elementos
        Utils.memcpy(m_Data, HEAD_LEN, Utils.ENDOFLINE, 0, Utils.ENDOFLINE.length);
        // se posiciona sobre el primer elemento
        m_File.seek(File.SEEK_START, 0);
        return (m_File.write(m_Data, 0, HEAD_LEN + Utils.ENDOFLINE.length) > 0);
    }
    
    /**
     * Almacena el identificador del siguiente registro a insertar.
     *
     * @param value entero que contiene el valor a colocar como
     *              cabecera.
     */
    private void headSaveNextID() {
        m_HeadNextID++;
        headSave();
    }
    
    /**
     * Ver documentación de <code>#headSet()</code>.
     *
     * @return       entero que contiene el valor de la cabecera.
     * @see         #headGet(int)
     */
    public int headGetNextId() {
        return m_HeadNextID;
    }
    
    /**
     * Inserta un nuevo registro en una tabla.
     *
     * @param id    identificador del registro.
     * @param line  arreglo de bytes que contiene la data.
     * @param start posición de <code>line</line> desde la cual comienza
     *              el registro.
     * @param count cantidad de bytes del registro.
     * @return      <code>true</code> si se puede almacenar el registro
     *              y <code>false</code>.
     */
    public boolean insert(StoreTxtId id, byte[] line, int start, int count) {
        // se inicializa <code>m_Record</code>
        recordInit(m_Data.length);
        /* Obtener un id para el nuevo registro. */
        if (insertMakeId(id, count)) {
            return insertToDisk(id, line, start, count);
        }
        return false;
    }
    
    /**
     * Inserta un nuevo registro en una tabla.
     *
     * @param id    identificador del registro.
     * @param line  arreglo de bytes que contiene la data.
     * @param count cantidad de bytes del registro.
     * @return      <code>true</code> si se puede almacenar el registro
     *              y <code>false</code> si no es posible.
     */
    public boolean insert(StoreTxtId id, byte[] line, int count) {
        return insert(id, line, 0, count);
    }
    
    /**
     * Verifica la posición donde es posible insertar nueva data
     * recorriendo el archivo buscando líneas eliminadas, de no
     * poder reutilizar una línea utiliza el final de registro.
     *
     *
     * @param id     se carga el id donde se debe escribir la data
     * @param m_Length longitud del registro a insertar
     * @return <code>true</code> si es posible insertar data y
     *               <code>false</code> si no es posible.
     */
    private boolean insertMakeId(StoreTxtId id, int length){
        StoreTxtId  backid;
        Buffer      buffer;
        
        /* Buscar primer registro encontrado */
        if (m_HeadDelete != Integer.MAX_VALUE) {
            backid = new StoreTxtId();
            buffer = new Buffer(length);
            
            /* Primer registro borrado */
            id.m_Offset  = m_HeadDelete;
            backid.m_Offset = id.m_Offset;
            
            while(seek(id.m_Offset)) {
                /* Leer registro borrado */
                buffer.clear();
                if (lineLoad(id, buffer, null)) {
                    if (id.m_Length > length) {
                        /** Utilizar espacio de registro borrado */
                        insertOverId(backid, id, buffer.bytes(),
                                buffer.offset() + id.m_RecLen - id.m_Length -
                                Utils.ENDOFLINE.length, id.m_Length);
                        /** Añadir registro sobre registro borrado */
                        buffer.consume(id.m_RecLen);
                        if (seek(id.m_Offset)) {
                            return true;
                        } else
                            break; /* Error de posicionamiento */
                    }
                    /** Siguiente registro borrado */
                    backid = id;
                    id.m_Offset = Utils.memtoul(buffer.bytes(),
                            buffer.offset(), id.m_Length, 16);
                } else {
                    // Error de lectura
                    break;
                }
            }
        }
        
        /* Añadir registro al final de la tabla */
        insertLength(id, length);
        if (m_File.seek(File.SEEK_END, 0)) {
            id.m_Offset = m_File.getOffset();
            return true;
        }
        return false;
    }
    
    private void insertOverId(StoreTxtId id, StoreTxtId backid,
            byte[] nextDelete, int start, int count){
        int pos;
        if (m_HeadDelete == id.m_Offset) {
            /** Actualizar la cabecera */
            m_HeadDelete = Utils.memtoul(nextDelete, start, 16);
            headSave();
        } else {
            pos = 0;
            /** Actualizar el registro borrado anterior. */
            pos += Utils.fmtInteger(m_Data, 4, id.m_RecLen, 10, 0, Utils.FMT_LEFT);
            Utils.memcpy(m_Data, pos, Utils.TOKEN, 0, 1);
            pos++;
            // Estado del registro
            pos += Utils.fmtInteger(m_Data, pos, 1, 1, 10, 0, Utils.FMT_LEFT);
            Utils.memcpy(m_Data, pos, Utils.TOKEN, 0, 1);
            pos++;
            // Registro borrado anterior
            pos += Utils.fmtInteger(m_Data, pos, HEAD_DELETE_LEN,
                    Utils.memtoul(nextDelete, start, count, 16),
                    10, 0, Utils.FMT_LEFT);
            Utils.memcpy(m_Data, pos, Utils.TOKEN, 0, 1);
            
            if (seek(backid.m_Offset)) {
                m_File.write(m_Data, pos);
            }
        }
    }
    
    /**
     * Verifica la posición donde es posible insertar nueva data
     * recorriendo el archivo buscando líneas eliminadas, de no
     * poder reutilizar una línea utiliza el final de registro.
     *
     * @param id     se carga el id donde se debe escribir la data
     * @param length longitud del registro a insertar
     * @return <code>true</code> si es posible insertar data y
     *               <code>false</code> si no es posible.
     */
    private void insertLength(StoreTxtId id, final int length) {
        int     infoLen;
        /**
         * Longitud de la data para usuario. Tiene que ser lo suficiente
         * para almacenar el offset de un registro en formato hexadecimal.
         */
        id.m_Length = length < 9 ? 9: length;
        // Longitud de un registro
        infoLen   = 3 + id.m_Length + Utils.ENDOFLINE.length;
        id.m_RecLen = Utils.fmtInteger(m_Data, 4, infoLen, 10, 0, Utils.FMT_LEFT) + infoLen;
        // Lo siguiente es por si recolen es 9 99 999 9999 ...
        if (id.m_RecLen != Utils.fmtInteger(m_Data, 4, id.m_RecLen, 10, 0, Utils.FMT_LEFT) + infoLen) {
            id.m_RecLen += 1;
        }
    }
    
    /**
     * Inserta un arreglo de bytes al disco, el arreglo será armado de
     * la siguiente manera <code>id.lenght</code> + separador de campo +
     * estado del registro + separador de campo + <code>line</code> +
     * separador de registro
     *
     * @param id    identificador del registro a almacenar
     * @param line  contenedor de la data del registro
     * @param start punto de <code>line</code> a partir del cual escribir
     * @param count cantidad de bytes de <code>line</code> a escribir
     * @return      <code>true</code> si se puede almacenar la data y
     *              <code>false</code> si no es posible almacenarla.
     */
    private boolean insertToDisk(StoreTxtId id, byte[] line, int start, int count) {
        int c = 0;    // posición a partir de la cual ir escribiendo
        // longitud del registro
        c = Utils.fmtInteger(m_Data, 4, id.m_RecLen, 10, 0, Utils.FMT_LEFT);
        m_BufRecord.append(m_Data, 0, c);
        // token de separador de campo
        m_BufRecord.append(Utils.TOKEN, Utils.TOKEN_PIPE, 1);
        // estado del registro : CERO por defecto
        c = Utils.fmtInteger(m_Data, 2, 0, 10, 0, Utils.FMT_LEFT);
        m_BufRecord.append(m_Data, 0, c);
        // token de separador de campo
        m_BufRecord.append(Utils.TOKEN, Utils.TOKEN_PIPE, 1);
        // se agrega la línea que contiene la data
        m_BufRecord.append(line, start, count);
        // espacio no utilizado se completa con separadores de campo
        while(count < id.m_Length) {
            m_BufRecord.append(Utils.TOKEN, Utils.TOKEN_PIPE, 1);
            count++;
        }
        m_BufRecord.append(Utils.ENDOFLINE, 0, Utils.ENDOFLINE.length);
        // se almacena en el archivo
        if (m_File.write(m_BufRecord.bytes(), 0, m_BufRecord.length()) > 0) {
            if (m_SaveNextID) {
                headSaveNextID();
            }
            return true;
        }
        return false;
    }
    
    /**
     * Devuelve el primer registro cuyo campo consultado coincida con
     * <code>value</code> para la función comparación seleccionada.
     * Existe dos tipos de comparación, exacta y no exacta. Si la
     * comparación es exacta, el valor evaluado debe coincidir con el
     * campo en longitud y tipo de letra. Sino, la coincidencia se da
     * si el valor evaluado forma parte del campo consultado.
     *
     * @param id    identificador del registro encontrado.
     * @param field posición del campo consultado.
     * @param value valor evaluado
     * @param start punto de <code>value</code> donde comienza el valor
     *              buscado
     * @param count longitud de bytes activos de value.
     * @param stop  posición límite
     * @param exact tipo de comparación.
     * @return      <code>true</code> si encuentra un registro y
     *              <code>false</code>  si no lo encuentra.
     * actue sobre el mismo objeto StoreTxt.
     */
    private boolean scanner(StoreTxtId id, int field, byte[] value,
            int start, int count, int stop, boolean exact) {
        int    i, pos;
        boolean cmp;
        //#ifdef DEBUG_STORETXT
//#         System.out.println("StoreTxt :: scanner() ENTERED");
        //#endif
        if (m_ScanBuffer == null) {
            if (!scanInit()) {
                return false;
            }
        } else {
            scanCont();
        }
        //#ifdef DEBUG_STORETXT
//#         System.out.println("\tBeginning Searching");
        //#endif
        // Seleccionar un registro
        while (scanLine(id)) {
            /**
             * Buscar coincidencia
             */
            if (value == null) {
                m_ScanOffset = m_File.getOffset();
                //#ifdef DEBUG_STORETXT
//#                 System.out.println("StoreTxt :: m_File.getOffset() -> " + m_ScanOffset);
                //#endif
                m_ScanBuffer.consume(id.m_RecLen);
                return true;
            } else {
                i   = m_ScanBuffer.offset()  + id.m_RecLen -
                        id.m_Length - Utils.ENDOFLINE.length;
                pos = field;
                while ((i + count <= id.m_Length + (m_ScanBuffer.offset()  + id.m_RecLen -
                        id.m_Length - Utils.ENDOFLINE.length)) && (pos < id.m_Length)) {
                    /**
                     * Ubicar el campo
                     */
                    if (pos == 0) {
                        /** Utilizar función de comparación */
                        if (exact) {
                            cmp = scanFind(m_ScanBuffer.bytes(), i, value, start, count);
                        } else {
                            cmp = scanLike(m_ScanBuffer.bytes(), i, value, start, count);
                        }
                        /** Se valida si el registro cumple la comparación */
                        if (cmp) {
                            m_ScanOffset = m_File.getOffset();
                            m_ScanBuffer.consume(id.m_RecLen);
                            return true;
                        }
                        break;
                    } else {
                        pos = (m_ScanBuffer.bytes()[i] == Utils.TOKEN[0]) ? (pos - 1): pos;
                    }
                    i++;
                }
            }
            m_ScanBuffer.consume(id.m_RecLen);
            // Límite de búsqueda.
            if ((stop > 0) && (id.m_Offset >= stop)) {
                break;
            }
        }
        // Detener el scanner.
        scanStop();
        return false;
    }
    
    private boolean scanInit() {
        scanStop();
        m_ScanBuffer = new Buffer(MINSTEP);
        if (m_ScanBuffer != null) {
            m_ScanOffset = m_File.getOffset();
            return true;
        }
        return false;
    }
    
    /**
     * Reinicializa cualquier búsqueda que se está realizando.
     */
    private void scanStop() {
        if (m_ScanBuffer != null) {
            m_ScanBuffer = null;
        }
        m_ScanOffset = Integer.MAX_VALUE;
    }
    
    private void scanCont() {
        seek(m_ScanOffset);
    }
    
    private boolean seek(int offset) {
        if (offset < Integer.MAX_VALUE) {
            offset = offset < HEAD_LEN + Utils.ENDOFLINE.length ?
                HEAD_LEN + Utils.ENDOFLINE.length : offset;
            //#ifdef DEBUG_STORETXT
//#             System.out.println("StoreTxt :: seek(" + offset + ")");
            //#endif
            return m_File.seek(File.SEEK_START, offset);
        }
        return false;
    }
    
    private boolean scanLine(StoreTxtId id) {
        byte[]  status = new byte[1];
        boolean line;
        //#ifdef DEBUG_STORETXT
//#         System.out.println("StoreTxt :: scanLine() ENTERED");
        //#endif
        // seleccionar
        status[0] = (byte) 48;
        line = lineLoad(id, m_ScanBuffer, status);
        while (line) {
            // busca hasta encontrar la primera línea eliminada
            if (status[0] == (byte) 48){
                break;
            } else {
                /** 
                 * Se consume para que cargue el siguiente registro
                 * ya que no va a llegar al scanner para que este lo
                 * consuma.
                 */
                m_ScanBuffer.consume(id.m_RecLen);
            }
            line = lineLoad(id, m_ScanBuffer, status);
        }
        //#ifdef DEBUG_STORETXT
//#         System.out.println("StoreTxt :: scanLine() EXITING");
        //#endif
        return line;
    }
    
    /**
     * Carga una línea en <buffer>.
     *
     * @param id        identificador del registro cargado.
     * @param buffer    buffer donde se carga el registro.
     * @param status    estado del registro
     * @return <code>true</code> si pudo cargar la línea y
     *         <code>false</code> si no puo cargarla
     * @see #lineLength(Buffer, int)
     */
    private boolean lineLoad(StoreTxtId id, Buffer buffer, byte[] status) {
        int[] iDigits = new int[1];
        int iLength;
        
        do {
            /**
             * Longitud del registro. Longitud del buffer mayor o igual
             * a la longitud del registro.
             */
            iDigits[0] = 0;
            id.m_RecLen = lineLength(buffer, iDigits);
            if ((id.m_RecLen > 0) && (id.m_RecLen <= buffer.length())) {
                // Offset del registro
                id.m_Offset = m_File.getOffset() - buffer.length();
                
                // Longitud de data del usuario
                id.m_Length = id.m_RecLen - iDigits[0] - 3 - Utils.ENDOFLINE.length;
                
                if (status != null){
                    // Estado del registro
                    status[0] = buffer.bytes()[buffer.offset() + iDigits[0] + 1];
                }
                // Data de usuario
                //buffer.consume(id.m_RecLen);
                return true;
            }
            // se lee la data del archivo y se almacena en <code>m_Buffer</code>
            //#ifdef DEBUG_STORETXT
//#             System.out.println("StoreTxt :: lineLoad() -> m_File.getOffset() -> " + m_File.getOffset());
//#             System.out.println("StoreTxt :: lineLoad() -> m_File.eof() -> " + m_File.eof());
//#             System.out.println("StoreTxt :: lineLoad() -> m_File.err() -> " + m_File.err());
            //#endif
            iLength = m_File.read(m_Data, 0, m_Data.length);
            //#ifdef DEBUG_STORETXT
//#             System.out.println("StoreTxt :: FileName -> " + m_File.getOffset() + ", EOF? " + m_File.eof());
//#             System.out.println("StoreTxt :: lineLoad() -> m_File.read() -> " + new String(m_Data, 0, iLength));
//#             System.out.println("StoreTxt :: lineLoad() -> m_File.getOffset() -> " + m_File.getOffset());
            //#endif
            if (iLength > 0) {
                buffer.append(m_Data, 0, iLength);
            }
        } while (iLength > 0);
        return false;
    }
    
    /**
     * Obtiene la longitud de una línea almacenada en un <code>buffer</code>.
     *
     * @param   buffer      buffer donde se encuentra el registro
     * @param   digits      [out] arreglo de enteros con un elemento que
     *                      contiene la cantidad de dígitos correspondientes
     *                      a la longitud de la línea.
     * @return              longitud del arreglo
     */
    private int lineLength(final Buffer buffer, int[] digits) {
        int  iTokPos, iRecLen;
        
        iRecLen = 0;
        if (buffer.length() > 0) {
            iTokPos = Utils.memmem(buffer.bytes(), buffer.offset(),
                    buffer.length(), Utils.TOKEN, Utils.TOKEN_PIPE, 1);
            digits[0] = 0;
            if (iTokPos > 0) {
                digits[0] = iTokPos - buffer.offset();
                iRecLen = Utils.memtoul(buffer.bytes(), buffer.offset(), digits[0], 10);
            }
        }
        return iRecLen;
    }
    
    private boolean scanFind(byte[] field, int start_field,
            byte[] value, int start_value, int count) {
        if (field[start_field + count] == Utils.TOKEN[0]) {
            if (Utils.memcmp(field, start_field, count, value, start_value, count) == 0) {
                return true;
            }
        }
        return false;
    }
    
    private boolean  scanLike(byte[] field, int start_field,
            byte[] value, int start_value, int count)  {
        if (Utils.memmem(field, start_field, count, Utils.TOKEN, 0, 1) < 0) {
            do {
                if (Utils.memnicmp(field, start_field, count,
                        value, start_value, count, count) == 0){
                    return true;
                }
                start_field++;
            } while (field[start_field + count - 1] != Utils.TOKEN[Utils.TOKEN_PIPE]);
        }
        return false;
    }
    
    /**
     * Busca un registro hasta el final de la tabla.
     *
     * @param id [out] Identificador del registro encontrado.
     * @param field   posición de campo a buscar.
     * @param value   flujo de bytes con el valor buscado.
     * @param start   punto de inicio del valor buscado.
     * @param count   tamaño del valor buscado.
     * @param exact   indica si es una búsqueda exacta.
     * @return        <code>true</code> si se encuentra coincidencia y
     *                <code>false</code> en caso contrario.
     */
    public boolean select(StoreTxtId id, int field,
            byte[] value, int start, int count, boolean exact){
        /* Consultar desde la la posici�n inicial hasta el final */
        return scanner(id, field, value, start, count, 0, exact);
    }
    
    public boolean select(StoreTxtId id, int field, byte[] value, int count, boolean exact){
        return select(id, field, value, 0, count, exact);
    }
    
    public boolean select(StoreTxtId id, int field, byte[] value, boolean exact){
        return select(id, field, value, 0, value.length, exact);
    }
    
    public boolean search(StoreTxtId id, int field,
            byte[] value, int start, int count, boolean exact){
        boolean pstr = false;
        int iCurrOffset;
        
        /** Obtener la posicion inicial */
        iCurrOffset = m_ScanOffset;
        /** Consultar desde la la posición inicial hasta el final */
        pstr = scanner(id, field, value, start, count, 0, exact);
        if (!pstr) {
            /* Reiniciar la busqueda desde el principio */
            offset(0);
            /* Consultar desde el principio hasta la posición inicial */
            pstr = scanner(id, field, value, start, count, iCurrOffset, exact);
        }
        return pstr;
    }
    
    public boolean search(StoreTxtId id, int field, byte[] value, int count, boolean exact) {
        return search(id, field, value, 0, count, exact);
    }
    
    public boolean search(StoreTxtId id, int field, byte[] value, boolean exact) {
        return search(id, field, value, 0, value.length, exact);
    }
    
    
    /**
     * Elimina un registro.
     *
     * @param id    Identificador válido de un registro.
     * @return      <code>true</code> si el registro pudo ser borrado,
     *              en caso contrario devuelve <code>false</code>.
     */
    public boolean delete(final StoreTxtId id){
        int c;
        if (id.isValid()) {
            recordInit(m_Data.length);
            c = 0;
            // longitud del registro
            c = Utils.fmtInteger(m_Data, 4, id.m_RecLen, 10, 0, Utils.FMT_LEFT);
            m_BufRecord.append(m_Data, 0, c);
            // token de separador de campo
            m_BufRecord.append(Utils.TOKEN, Utils.TOKEN_PIPE, 1);
            // estado del registro : uno para eliminados
            c = Utils.fmtInteger(m_Data, 2, 1, 10, 0, Utils.FMT_LEFT);
            m_BufRecord.append(m_Data, 0, c);
            // token de separador de campo
            m_BufRecord.append(Utils.TOKEN, Utils.TOKEN_PIPE, 1);
            // Header
            c = Utils.fmtInteger(m_Data, HEAD_DELETE_LEN, m_HeadDelete, 16, 0, Utils.FMT_LEFT);
            m_BufRecord.append(m_Data, 0, c);
            // token de separador de campo
            m_BufRecord.append(Utils.TOKEN, Utils.TOKEN_PIPE, 1);
            /** Posicionarse en el archivo. */
            if (seek(id.m_Offset)) {
                /** Escribir el registro. */
                c = m_File.write(m_BufRecord.bytes(), 0, m_BufRecord.length());
                // se libera memoria para el buffer
                m_BufRecord = null;
                /** Actualizar la cabecera. */
                m_HeadDelete = id.m_Offset;
                headSave();
                return true;
            }
        }
        return false;
    }
    
    /**
     * Actualiza un registro en la tabla.
     *
     * @param id    identificador del registro.
     * @param line  arreglo de bytes que contiene la data.
     * @param start posición de <code>line</line> desde la cual comienza
     *              el registro.
     * @param count cantidad de bytes del registro.
     * @return      <code>true</code> si se puede actualizar el registro,
     *              caso contrario retorna <code>false</code>.
     * @see #delete(StoreTxtId)
     * @see #insert(StoreTxtId, byte[], int, int)
     */
    public boolean update(StoreTxtId id, byte[] line, int start, int count){
        if (id.isValid() && (count < Integer.MAX_VALUE)) {
            if (count > id.m_Length) {
                if (delete(id)) {
                    return insert(id, line, start, count);
                }
            } else {
                if (seek(id.m_Offset)) {
                    recordInit(m_Data.length);
                    return insertToDisk(id, line, start, count);
                }
            }
        }
        return false;
    }
    
    public boolean update(StoreTxtId id, byte[] line, int count){
        return update(id, line, 0, count);
    }
    
    public boolean update(StoreTxtId id, byte[] line){
        return update(id, line, 0, line.length);
    }
    
    public boolean offset(int offset){
        if (seek(offset)) {
            scanStop();
            return true;
        }
        return false;
    }
    
    /**
     * Carga en un <code>DataTxt</code> un registro que pertenece a
     * un offset determinado de la tabla.
     *
     * @param offset  offset del registro en la tabla.
     * @param data    almacena la información generada.
     * @return        <code>true</code> si se pudo cargar el registro
     *                y <code>false</code> en caso contrario.
     */
    public boolean load(int offset, DataTxt data) {
        //#ifdef DEBUG_STORETXT
//#         System.out.println("StoreTxt :: load() ENTERED");
        //#endif
        byte[] status = new byte[1];
        boolean ret = false;
        status[0] = (byte)48;
        if (m_BufShared == null) {
            m_BufShared = new Buffer(m_Data.length);
        }
        if(offset >= 0 && seek(offset)) {
            //#ifdef DEBUG_STORETXT
//#             System.out.println("\tseek() ->" + offset);
            //#endif
            if(lineLoad(data.m_RecordID, m_BufShared, status)) {
                //#ifdef DEBUG_STORETXT
//#                 System.out.println("\tlineLoad() ->" + data.m_RecordID);
                //#endif
                if(status[0] == (byte)48){
                    data.make(m_BufShared.bytes(),
                            m_BufShared.offset() + data.m_RecordID.m_RecLen -
                            data.m_RecordID.m_Length - Utils.ENDOFLINE.length,
                            data.m_RecordID.m_Length);
                    ret = true;
                }
            }
        }
        m_BufShared = null;
        //#ifdef DEBUG_STORETXT
//#         System.out.println("StoreTxt :: load() EXITING");
        //#endif
        return ret;
    }
    
    /**
     * Ver la documentación de #load(int, DataTxt).
     *
     * @param id    identificador del registro.
     * @param data  almacena la información generada.
     * @return      <code>true</code> si se pudo cargar el registro
     *              y <code>false</code> en caso contrario.
     */
    public boolean load(final StoreTxtId id, DataTxt data) {
        return load(id.m_Offset, data);
    }
    
    public boolean load(DataTxt data) {
        return load(data.m_RecordID, data);
    }
    
    /**
     * Guarda en disco la información de un <code>DataTxt</code>.
     *
     * @param data  contiene el registro a ser almacenado.
     * @return  la cantidad de bytes almacenados.
     */
    public boolean save(DataTxt data) {
        if (data.m_RecordID.m_Length < data.m_RecordID.m_RecLen 
                || data.m_RecordID.m_RecLen  == StoreTxtId.INVALID){
            return insert(data.m_RecordID, data.toRecord().bytes(), data.toRecord().length());
        }
        return update(data.m_RecordID, data.toRecord().bytes(), data.toRecord().length());
    }
    
    /**
     * Elimina en disco la información de un <code>DataTxt</code>.
     *
     * @param data  contiene el registro a ser eliminado.
     * @return      <code>true</code> si se pudo eliminar el registro,
     *              <code>false</code> si no fue posible.
     */
    public boolean kill(final DataTxt data) {
        if (data.m_RecordID.isValid() && delete(data.m_RecordID)) {
            data.m_RecordID.setInvalid();
            return true;
        }
        return false;
    }
    
    /**
     * Limpia el contenido del buffer de registro.
     *
     * @param step  tamaño del buffer de no existir este en memoria.
     */
    private void recordInit(int step) {
        if (m_BufRecord == null) {
            m_BufRecord = new Buffer(m_Data.length);
        }
        m_BufRecord.clear();
    }
}
